﻿namespace SpikingAnalyticsLib

open System
open System.IO
open System.Text
open System.Collections.Generic
open SpikingNeuronLib
open GeneralUtilities   // for conversions
open IOUtilities
open NetworkUtilities

//=================================================================================================

// Parameters that specify some action to perform such as saving the network state
// Actions are invoked within an event handler (see e.g. CrossbarNetwork.Train())
type NetworkStateActionParameters =
    struct
        val BindDefaultAction:bool
        val PerformAction:bool
        val ActionInterval:int
        val RepeatedActionBeforeCleanup:(int -> float -> float -> float -> CrossbarNetwork -> unit) option     // before cleanup action repeated every ActionInterval seconds
        val RepeatedActionAfterCleanup:(int -> CrossbarNetwork -> unit) option      // after cleanup action repeated every ActionInterval seconds
        val FinalAction:(int -> CrossbarNetwork -> unit) option                     // action run once on completion e.g. a final save

        new(performAction, actionInterval) =
            {
                BindDefaultAction = true
                PerformAction = performAction
                ActionInterval = actionInterval
                RepeatedActionBeforeCleanup = None
                RepeatedActionAfterCleanup = None
                FinalAction = None
            }

        new(bindDefaultAction, performAction, actionInterval) =
            {
                BindDefaultAction = bindDefaultAction
                PerformAction = performAction
                ActionInterval = actionInterval
                RepeatedActionBeforeCleanup = None
                RepeatedActionAfterCleanup = None
                FinalAction = None
            }

        new(performAction, actionInterval, repeatedActionBeforeCleanup, repeatedActionAfterCleanup, finalAction) =
            {
                BindDefaultAction = true
                PerformAction = performAction
                ActionInterval = actionInterval
                RepeatedActionBeforeCleanup = repeatedActionBeforeCleanup
                RepeatedActionAfterCleanup = repeatedActionAfterCleanup
                FinalAction = finalAction
            }

        /// Create a new instance that binds the specified tupled action method to the repeated action (after cleanup)
        // The other parameter values are retained i.e. otherwise a copy of the old instance
        member this.BindRepeatedActionAfterCleanup(actionMethod:(int * CrossbarNetwork * string * string) -> unit, folder, baseName) =
            // the action captures folder and basename from this instance
            let repeatedAction = NetworkStateActionParameters.CreateActionFromMethod(actionMethod, folder, baseName)
            new NetworkStateActionParameters(this.PerformAction, this.ActionInterval,
                this.RepeatedActionBeforeCleanup, repeatedAction, this.FinalAction)

        /// Create a new instance that binds the specified tupled action method to the final action
        // The other parameter values are retained i.e. otherwise a copy of the old instance
        member this.BindFinalActionAfterCleanup(actionMethod:(int * CrossbarNetwork * string * string) -> unit, folder, baseName) =
            // the action captures folder and basename from this instance
            let finalAction = NetworkStateActionParameters.CreateActionFromMethod(actionMethod, folder, baseName)
            new NetworkStateActionParameters(this.PerformAction, this.ActionInterval,
                this.RepeatedActionBeforeCleanup, this.RepeatedActionAfterCleanup, finalAction)

        /// Create an action from a method taking a four-tuple argument
        static member CreateActionFromMethod(actionMethod:(int * CrossbarNetwork * string * string) -> unit, folder, baseName) =
            let wrapper = fun sec network folder baseName -> actionMethod(sec, network, folder, baseName)
            let networkAction sec network = wrapper sec network folder baseName
            Some(networkAction)

        /// Create a new instance with the specified parameters and with (after cleanup) actions that call the specified tupled functions
        static member Create(outputFolder, baseName, performAction, actionInterval, repeatedActionMethodAfterCleanup, finalActionMethod) =
            let repeatedAction = NetworkStateActionParameters.CreateActionFromMethod(repeatedActionMethodAfterCleanup, outputFolder, baseName)
            let finalAction = NetworkStateActionParameters.CreateActionFromMethod(finalActionMethod, outputFolder, baseName)

            new NetworkStateActionParameters(performAction, actionInterval,
                None, repeatedAction, finalAction)
    end

//=================================================================================================

/// A CrossbarNetwork super class with additional features
// Note that NetworkStateActionParameters forward declares CrossbarNetwork
// creating a dependancy between NetworkStateActionParameters and CrossbarNetwork
and [<AllowNullLiteral>] CrossbarNetwork internal (networkSpecifier:CrossbarNetworkSpecifier, millisecTickCollector:OneMillisecTickDataCollector option, initialise, verbose) as this =
    inherit SpikingNeuronLib.CrossbarNetwork(networkSpecifier.NumExcitatoryNeurons, networkSpecifier.NumInhibitoryNeurons, networkSpecifier.MaxDelay)

    let SetDataCollectionEnabled firingEventCollectionHandler enabled =
        if enabled then
            this.add_OneSecondTickBeforeCleanup(firingEventCollectionHandler)
        else
            this.remove_OneSecondTickBeforeCleanup(firingEventCollectionHandler)

    let firingEventCollector = new OneSecondTickDataCollector()
    let firingEventCollectionHandler =
        let oneSecTickHandler = firingEventCollector.SecondTickHandler (upcast this) verbose   // capture the network reference
        new OneSecondTickBeforeCleanupEventHandler(oneSecTickHandler)

    let EnableDataCollection () =
        // disable learning
        this.PlasticityDisabled <- true

        firingEventCollector.Clear()
        SetDataCollectionEnabled firingEventCollectionHandler true

    let DisableDataCollection () =
        // re-enable learning
        this.PlasticityDisabled <- false

        SetDataCollectionEnabled firingEventCollectionHandler false

    // a unique file header to ensure a compressed file contains CrossbarNetwork network state data
    let uniqueID = [| 194uy; 189uy; 154uy; 1uy; 154uy; 139uy; 117uy; 64uy; 177uy; 246uy; 234uy; 196uy; 229uy; 99uy; 77uy; 184uy; |]

    do
        this.add_RunWarning(new RunWarningEventHandler(fun msg -> printfn "%s" msg))

        if networkSpecifier.Options.IsSome then
            let options = networkSpecifier.Options.Value
            this._metaplasticityEnabled <- options.MetaplasticityEnabled
            this._minSynapticWeightSoftLimit <- options.SynapticWeightLimitMinSoft
            this._maxSynapticWeightSoftLimit <- options.SynapticWeightLimitMaxSoft
            this._maxSynapticWeightHardLimit <- options.SynapticWeightLimitMaxHard

        // optionally add connections
        if initialise then
            if networkSpecifier.Connections.IsNone then
                // add random connections for a grid network
                this.AddFixedConnectionList(int (float networkSpecifier.TotalNeurons * 0.1), networkSpecifier.DefaultExcitatoryWeight, networkSpecifier.DefaultInhibitoryWeight)
            else
                this.AddConnections(networkSpecifier.Connections.Value)

        // optionally add a high resolution collector
        if millisecTickCollector.IsSome then
            let millisecondHandler = millisecTickCollector.Value.MillisecTickHandler (upcast this)       // capture the network reference
            this.add_OneMillisecondTick(new OneMillisecondTickEventHandler(millisecondHandler))

//---------------------------------------------------------------------------------------------------------
// Constructors

    /// Create a new CrossbarNetwork with the specified network specifications
    new(networkSpecifier:CrossbarNetworkSpecifier) =
        new CrossbarNetwork(networkSpecifier, None, true, false)

    /// Create a new CrossbarNetwork with the specified network specifications and with console messages enabled or disabled
    new(networkSpecifier:CrossbarNetworkSpecifier, verbose) =
        new CrossbarNetwork(networkSpecifier, None, true, verbose)

    /// Create a new CrossbarNetwork with the specified network specifications
    /// and high resolution data collection parameters
    /// and with console messages enabled or disabled
    new(networkSpecifier:CrossbarNetworkSpecifier, highResolutionDataCollectionParameters:OneMillisecTickDataCollectorParameters, verbose) =
        let millisecTickCollector = Some(new OneMillisecTickDataCollector(highResolutionDataCollectionParameters))
        new CrossbarNetwork(networkSpecifier, millisecTickCollector, true, verbose)

//---------------------------------------------------------------------------------------------------------
// Internal members

    member internal this.FiringEventList
        with get() = firingEventCollector.FiringEvents

    /// Run the network with the specified stimulus and learning disabled whilst accumulating firing events
    member internal this.CollectFiringData(totalSeconds, backgroundFiringRate, inputPattern) =
        let patternGenerator = Some(new PoissonProcessPatternGenerator(networkSpecifier.TotalNeurons, backgroundFiringRate))

        EnableDataCollection ()
        RunNetwork this totalSeconds patternGenerator inputPattern
        DisableDataCollection ()

    /// Run the network with randomly-selected patterns from inputPatterns returning the selected patterns and the accumulated firing data
    member internal this.CollectFiringDataWithRandomSelection(totalSeconds, backgroundFiringRate, inputPatterns) =
        let patternGenerator = Some(new PoissonProcessPatternGenerator(networkSpecifier.TotalNeurons, backgroundFiringRate))

        // generate some test data: randomly select an input pattern and record the selection against the frame number
        // input patterns have an input period of 1000 msec for convenience in matching with data collection
        printf "Collecting test data over %d seconds" totalSeconds
        EnableDataCollection ()
        let selectedPatternBySecond, collectedData = CollectFixedFrameDataRandomSelection this firingEventCollector.FiringEvents totalSeconds patternGenerator inputPatterns
        DisableDataCollection ()
        printfn "done"

        selectedPatternBySecond, collectedData

    /// Run the network with the specified patterns in the specified order returning the selected patterns and the accumulated firing data
    member internal this.CollectFiringDataWithOrderedSelection(secondsPerPattern, backgroundFiringRate, inputPatterns:Map<_, _>, stimulusOrder:'a list) =
        let patternGenerator = Some(new PoissonProcessPatternGenerator(networkSpecifier.TotalNeurons, backgroundFiringRate))

        // generate some test data: run each input pattern in order for the specified number of seconds per pattern
        // input patterns have an input period of 1000 msec for convenience in matching with data collection
        printf "Collecting test data over %d seconds" (stimulusOrder.Length * secondsPerPattern)
        EnableDataCollection ()
        let selectedPatternBySecond, collectedData = CollectFixedFrameDataOrderedSelection this firingEventCollector.FiringEvents patternGenerator inputPatterns stimulusOrder secondsPerPattern
        DisableDataCollection ()
        printfn "done"

        selectedPatternBySecond, collectedData

//---------------------------------------------------------------------------------------------------------
// Properties

    /// Get the network specifier used to construct this network
    member this.NetworkSpecifier
        with get() = networkSpecifier

    /// Get whether connections were added at network construction time
    member this.ConnectionsAdded
        with get() = initialise

    /// Get whether the network was constructed in verbose mode
    member this.Verbose
        with get() = verbose

    /// Get the nested type that collects low resolution events
    member this.OneSecondEventCollector
        with get() = firingEventCollector

    /// Get the nested type that collects high resolution events
    member this.OneMillisecondEventCollector
        with get() = millisecTickCollector

    /// Get all incoming connections i.e. a list of connections keyed by post-synaptic neuron
    member this.GetIncomingConnections
        with get() =
            GetConnectionsByNeuron this (fun connection -> connection.PostNeuron)

    /// Get all outgoing connections i.e. a list of connections keyed by pre-synaptic neuron
    member this.GetOutgoingConnections
        with get() =
            GetConnectionsByNeuron this (fun connection -> connection.PreNeuron)

    /// Return a Dictionary of incoming connections for each neuron
    member this.GetIncomingConnectionsAsDictionary
        with get() =
            ConvertMappedValues ConvertListToEnumerable this.IncomingConnections
            |> ConvertMapToDictionary 

    /// Return a Dictionary of outgoing connections for each neuron
    member this.GetOutgoingConnectionsAsDictionary
        with get() =
            ConvertMappedValues ConvertListToEnumerable this.OutgoingConnections
            |> ConvertMapToDictionary 

    /// Get the accumulated firing events from the last run as a sequence of FiringEvents
    member this.FiringEvents
        with get() =
            this.FiringEventList
            |> Seq.map (fun (time, nindex, isBackgroundEvent) -> new FiringEvent(time, nindex, isBackgroundEvent))

    /// Get the accumulated firing events from the last run as a list of tuples
    member this.EventTuples = this.FiringEventList

    member internal this.IncomingConnections
        with get() =
            GetConnectionsByNeuron this (fun connection -> connection.PostNeuron)

    member internal this.OutgoingConnections
        with get() =
            GetConnectionsByNeuron this (fun connection -> connection.PreNeuron)

//---------------------------------------------------------------------------------------------------------
// Methods

    /// Enable or disable synaptic plasticity
    member this.SetPlasticityDisabled(disabled) = this.PlasticityDisabled <- disabled

    member this.Clone() =
        let network = new CrossbarNetwork(networkSpecifier, None, false, false)
        let networkBase:SpikingNeuronLib.CrossbarNetwork = upcast network

        // copy the base data
        base.CopyTo(networkBase)
        network

    /// Save the network state to a compressed binary file
    member this.SaveCompressedNetworkState(filePath) =
        // save to memory stream
        use saveStream = new MemoryStream()
        use sw = new StreamWriter(saveStream, Encoding.UTF8)
        this.SaveNetworkState(sw)
        sw.Close()  // ensure the writer is flushed to the memory stream

        // compress the data
        use zippedStream = ZipData (saveStream.ToArray())

        // add unique signature to the header
        let combined = Array.concat [ uniqueID; zippedStream.ToArray(); ]

        // save the compressed data to a file
        File.WriteAllBytes(filePath, combined)

    /// Load the network state from a compressed binary file
    member this.LoadCompressedNetworkState(filePath) =
        // read the binary file
        let zipped = File.ReadAllBytes(filePath)

        // check the first 16 bytes match the unique ID
        let headerCorrect = Array.forall2 (fun a b -> a = b) uniqueID zipped.[..15]

        if headerCorrect then

            // uncompress the data
            use loadStream = UnzipData zipped.[16..]    // skip the first 16 bytes

            // load from memory stream
            loadStream.Seek(0L, SeekOrigin.Begin) |> ignore
            use sr = new StreamReader(loadStream, Encoding.UTF8)
            this.LoadNetworkState(sr)

        else
            raise (System.ArgumentException("The specified file does not contain compressed network state data."))

    /// Derive the pre-synaptic impulses arriving at the specified neuron given the specified firing events
    /// Utilises the delays and synaptic weights of the connections onto the neuron to compute the size and arrival time
    /// of each impulse generated by the earlier firing of the pre-synaptic neurons
    member this.GetImpulsesOnNeuron(neuronIndex, firingEvents) =
        let incomingConnections =
            let allConnections = GetConnectionsByNeuron this (fun connection -> connection.PostNeuron)
            allConnections.[neuronIndex]

        let preSynapticNeurons =
            incomingConnections
            |> Seq.map (fun connection -> connection.PreNeuron)
            |> Set.ofSeq

        let delayMap =
            incomingConnections
            |> Seq.map (fun connection -> connection.PreNeuron, int connection.Delay)
            |> Map.ofSeq

        let weightMap =
            incomingConnections
            |> Seq.map (fun connection -> connection.PreNeuron, connection.Weight)
            |> Map.ofSeq

        firingEvents
        |> Seq.filter (fun (time, nindex) -> preSynapticNeurons.Contains nindex)
        |> Seq.map (fun (time, nindex) ->
            time + delayMap.[nindex], weightMap.[nindex]
        )
        |> Seq.toList

    /// Use the underlying network connectivity to generate a structural description of the specified firing events
    /// showing only links with weights greater than the specified synaptic threshold
    member this.GetPNGDescriptor(groupFiringEvents:seq<int * int * float>, synapticThreshold, ?jitter:int) =

        let allowedJitter = if jitter.IsSome then jitter.Value else 2  // was this.MaximumAxonalDelay
        let outgoingConnections = GetConnectionsByNeuron this (fun connection -> connection.PreNeuron)
        let minTime, maxTime, linkedEventsList = GetGroupLinksFromPostConnectionMap allowedJitter synapticThreshold outgoingConnections groupFiringEvents
        SpikingAnalyticsLib.PNGDescriptor(minTime, maxTime, linkedEventsList)

    /// Use the underlying network connectivity to generate a structural description of the specified firing events
    /// showing only links with weights greater than the specified synaptic threshold
    member this.GetPNGDescriptor(groupFiringEvents:seq<int * int>, synapticThreshold, ?jitter:int) =

        let allowedJitter = if jitter.IsSome then jitter.Value else 2  // was this.MaximumAxonalDelay
        let groupEvents = Seq.map (fun (time, neuronIndex) -> time, neuronIndex, 1.0) groupFiringEvents
        let outgoingConnections = GetConnectionsByNeuron this (fun connection -> connection.PreNeuron)
        let minTime, maxTime, linkedEventsList = GetGroupLinksFromPostConnectionMap allowedJitter synapticThreshold outgoingConnections groupEvents
        SpikingAnalyticsLib.PNGDescriptor(minTime, maxTime, linkedEventsList)

    /// Use the underlying network connectivity to generate a structural description of the specified frame group
    /// showing only links with weights greater than the specified synaptic threshold
    member this.GetPNGDescriptor(frameGroups:(int * int) list list, index, synapticThreshold) =
        this.GetPNGDescriptor(frameGroups.[index], synapticThreshold)

    /// Use the underlying network connectivity to generate a structural description of the specified firing events
    // i.e. link events that are causally related
    member this.GetPNGDescriptor(groupFiringEvents:seq<FiringEvent>) =
        this.GetPNGDescriptor(groupFiringEvents, 0.0)

    /// Use the underlying network connectivity to generate a structural description of the specified firing events
    /// showing only links with weights greater than the specified synaptic threshold
    member this.GetPNGDescriptor(groupFiringEvents:seq<FiringEvent>, synapticThreshold) =

        let groupEvents = Seq.map (fun (evt:FiringEvent) -> evt.Time, evt.NeuronIndex) groupFiringEvents
        this.GetPNGDescriptor(groupEvents, synapticThreshold)

    /// Use the underlying network connectivity to generate a structural description of the specified firing event probability list
    /// showing only links with weights greater than the specified synaptic threshold
    // A firing event probability list is a list of (FiringEvent, activationProbability) pairs (e.g. see classifier.GetAveragedFrameGroups())
    member this.GetPNGDescriptor(groupFiringEvents:seq<FiringEvent * float>, synapticThreshold) =

        let groupEvents = Seq.map (fun (evt:FiringEvent, _) -> evt.Time, evt.NeuronIndex) groupFiringEvents
        this.GetPNGDescriptor(groupEvents, synapticThreshold)

    /// Given a stimulus pattern and the generated firing events, use the network structure to partition the events into first layer or other layer
    // First layer events are those in which the fired neuron has a direct connection to a stimulus firing event
    member this.GetLayeredFiringEvents(pattern:Pattern, firingEvents:(int * int * float) list, synapticThreshold) =
        let stimulusSet =
            pattern.GetEventTuples()
            |> Set.ofSeq

        let allFiringEvents =
            let inputEvents =
                stimulusSet
                |> Seq.map (fun (time, nindex) -> time, nindex, 1.0)    // default link probability of 1.0
                |> Seq.toList
            List.append inputEvents firingEvents

        let groupDescriptor = this.GetPNGDescriptor(allFiringEvents, synapticThreshold)

        // for each link select the post-synaptic event wherever the pre-synaptic event is in the stimulus set
        let firstLevelEvents =
            groupDescriptor.LinkedEventsAsTuples
            |> Seq.filter (fun (pre, post, weight, probability) -> stimulusSet.Contains pre && (not (stimulusSet.Contains post)))
            |> Seq.map (fun (pre, (time, nindex), weight, probability) -> time, nindex, probability)
            |> Set.ofSeq

        let otherEvents =  // second level and beyond
            let allWindows =
                firingEvents
                |> Set.ofSeq
            Set.difference allWindows firstLevelEvents

        firstLevelEvents, otherEvents

    /// Train this network and perform the specified action (e.g. saving the network) at regular intervals (specified by the one second tick)
    /// WARNING: to prevent calling this method recursively, avoid invoking any firing data collection methods in the action handler
    // For saving state use something like:
    //  let networkAction sec network = CrossbarNetwork.SaveState(sec, network, folder, baseName)
    //  let actionParameters = new NetworkStateActionParameters(performAction, actionInterval, None, networkAction, None)
    member this.Train(inputPattern:IStimulus, runSeconds, backgroundFiringRate, actionParameters:NetworkStateActionParameters) =
        let stimulus =
            if inputPattern = null then
                None
            else
                Some(inputPattern)

        this.RunWithAction(runSeconds, stimulus, backgroundFiringRate, actionParameters)

    /// Train this network and save the final network state
    member this.Train(inputPattern:IStimulus, runSeconds, backgroundFiringRate, outputFolder, networkName) =
        // create a final save action
        let networkAction sec network = CrossbarNetwork.SaveState(sec, network, outputFolder, networkName)
        let saveParameters = new NetworkStateActionParameters(false, 0, None, None, Some(networkAction))

        this.Train(inputPattern, runSeconds, backgroundFiringRate, saveParameters)

    /// Train this network with no actions e.g. just collecting membrane data or synaptic weight data
    member this.TrainWithNoAction(inputPattern:IStimulus, runSeconds, backgroundFiringRate) =
        let trainParameters = new NetworkStateActionParameters(false, false, 0)
        this.Train(inputPattern, runSeconds, backgroundFiringRate, trainParameters)

    /// Run the network with arbitrary actions on each one second tick e.g. save network state
    member this.RunWithAction(runSeconds, stimulus:IStimulus option, backgroundFiringRate, actionParameters:NetworkStateActionParameters, ?collectFiringEvents, ?plasticityEnabled) =

        let patternGenerator =
            if backgroundFiringRate > 0 then
                Some(new PoissonProcessPatternGenerator(this.TotalNeurons, backgroundFiringRate))
            else
                None

        // event bind any repeated action
        CrossbarNetwork.AddActionHandlers(this, actionParameters)

        // if requested then enable firing event collection
        let enableFiringEventCollection = if collectFiringEvents.IsSome then collectFiringEvents.Value else false
        firingEventCollector.Clear()

        // enable/disable learning
        let enablePlasticity = if plasticityEnabled.IsSome then plasticityEnabled.Value else true
        this.PlasticityDisabled <- not enablePlasticity

        SetDataCollectionEnabled firingEventCollectionHandler enableFiringEventCollection
        CrossbarNetwork.RunNetworkWithFinalAction(this, runSeconds, patternGenerator, stimulus, actionParameters)
        SetDataCollectionEnabled firingEventCollectionHandler false

    /// Run the network with an optional stimulus and with plasticity enabled or disabled (enabled by default)
    // Firing event collection is enabled; the handler for the OneSecondTickDataCollector is bound to the OnOneSecondTickBeforeCleanup event
    member this.Run(runSeconds, stimulus:IStimulus option, backgroundFiringRate, ?plasticityEnabled) =

        let patternGenerator =
            if backgroundFiringRate > 0 then
                Some(new PoissonProcessPatternGenerator(this.TotalNeurons, backgroundFiringRate))
            else
                None

        // enable/disable learning
        let enablePlasticity = if plasticityEnabled.IsSome then plasticityEnabled.Value else true
        this.PlasticityDisabled <- not enablePlasticity

        firingEventCollector.Clear()

        SetDataCollectionEnabled firingEventCollectionHandler true
        RunNetwork this runSeconds patternGenerator stimulus
        SetDataCollectionEnabled firingEventCollectionHandler false

    /// Collect some firing data in response to the specified pattern
    /// over multiple frames with each frame having the specified frame size in milliseconds
    member this.GetFrameSet(pattern:Pattern, numberOfFrames, frameSize, backgroundFiringRate) =

        printf "Collecting data..."

        // run the network with random background (no stimulus) in order to initialise the recurrent inputs
        this.Run(10, None, 1, false)

        let oneSecondInputStimulus =
            let stimulus = Stimulus.Create(pattern, frameSize)
            Some(stimulus :> IStimulus)

        this.Run(numberOfFrames, oneSecondInputStimulus, backgroundFiringRate, false)
        let collectedData = this.FiringEventList

        printfn "done"

        // select all neurons
        let selectedNeurons = [ for i in 0..this.TotalNeurons-1 -> i ]

        new FrameSet(selectedNeurons, this.TotalNeurons, frameSize, numberOfFrames, collectedData)

    /// Get connections that may be activated in the specified response frame
    /// where the frame contains firing events generated by a known stimulus
    /// Return connection indices that are activated with the specified jitter in the firing time
    member this.GetActivatedConnections(frame:Frame, jitter) =

        let delayOffsets = [ for i in 0..jitter -> i ]

        this.GetActivatedConnections(frame, delayOffsets)

    /// Get connections that may be activated in the specified response frame
    /// where the frame contains firing events generated by a known stimulus
    /// Return connection indices that are activated within the specified range of offsets in the firing time
    // What is the proportion of frames in which preNeuron and postNeuron both fire with a firing time diff compatible w/ the connection delay?
    // See also GetFrameSet method
    member this.GetActivatedConnections(frame:Frame, delayOffsets) =

        let offsetListByNeuron = frame.OffsetMap

        this.Connections
        |> Seq.mapi (fun index connection ->
            let delaySet =
                let delay = int connection.Delay
                // to be causative, the impulse must arrive before (or at the same time as) the post-synaptic firing event
                // delay; delay + 1; delay + 2; ...
                delayOffsets
                |> List.map (fun offset -> delay + offset)
                |> Set.ofList
            index, connection.PreNeuron, connection.PostNeuron, delaySet
        )
        |> Seq.map (fun (index, pre, post, delays) ->
            let incompatible =
                if offsetListByNeuron.ContainsKey pre && offsetListByNeuron.ContainsKey post then
                    seq {
                        for preFiringTime in offsetListByNeuron.[pre] do
                            for postFiringTime in offsetListByNeuron.[post] do
                                yield postFiringTime - preFiringTime
                    }
                    // the firing time difference must be equal to the delay, or be greater (equal to delay + 1; delay + 2; etc)
                    // so that the post-synaptic firing time occurs at or after the impulse arrival time
                    |> Seq.filter (fun firingTimeDifference -> delays.Contains firingTimeDifference)
                    |> Seq.isEmpty
                else
                    true
            index, not incompatible
        )
        |> Seq.filter (fun (index, compatible) -> compatible)
        |> Seq.map (fun (index, _) -> index)
        |> Set.ofSeq

    /// Get connections that are regularly activated in the specified set of response frames
    /// where each frame contains firing events generated by a known stimulus
    /// Return connection indices that are activated in the specified proportion of frames with the specified jitter in the firing time
    member this.GetActivatedConnections(frameSet:FrameSet, proportion, jitter) =
        assert (proportion > 0.0 && proportion <= 1.0)

        let connectionCounts:int [] = Array.zeroCreate this.Connections.Count

        let threshold =
            let numberOfFrames = frameSet.Length
            int (proportion * float numberOfFrames)

        let delayOffsets = [ for i in 0..jitter -> i ]
        printfn "Processing (threshold = %d)..." threshold

        frameSet.ToEnumerable()
        |> Seq.iteri (fun frameIndex (frame:Frame) ->
            printfn "Frame %d" frameIndex
            this.GetActivatedConnections(frame, delayOffsets)
            |> Seq.iter (fun (connectionIndex) -> connectionCounts.[connectionIndex] <- connectionCounts.[connectionIndex] + 1)
        )

        let activatedConnectionOffsets =
            connectionCounts
            |> Seq.mapi (fun index count -> index, count)
            |> Seq.filter (fun (index, count) -> count > threshold)     // threshold = 0 -> 1's, 9's and 10's; suggest proportion of 0.8
            |> Seq.map (fun (index, count) -> index)
            |> Set.ofSeq

        activatedConnectionOffsets

    /// Compute the difference in firing time for the pre and post neurons for each connection
    /// using firing data generated over multiple frames
    // The mode firing time difference is selected over the specified frames
    member this.GetConnectionFiringDiffs(frameSet:FrameSet) =

        // extract offsets for each frame
        frameSet.ToEnumerable()
        |> Seq.mapi (fun index frame ->
            index, frame.OffsetMap
        )
        // for each frame, get the firing time diffs for each connection's pre and post neuron
        |> Seq.map (fun (frameIndex, offsetListByNeuron:Map<int, int list>) ->
            printfn "Frame %d" frameIndex
            let firingTimeDiffsByConnection =
                this.Connections
                |> Seq.mapi (fun connectionIndex connection -> connectionIndex, connection.PreNeuron, connection.PostNeuron)
                |> Seq.map (fun (connectionIndex, pre, post) ->
                    let firingTimeDiffs =
                        if offsetListByNeuron.ContainsKey pre && offsetListByNeuron.ContainsKey post then
                            seq {
                                for preFiringTime in offsetListByNeuron.[pre] do
                                    for postFiringTime in offsetListByNeuron.[post] do
                                        yield postFiringTime - preFiringTime
                            }
                            |> Seq.toList
                        else
                            []
                    connectionIndex, firingTimeDiffs
                )
                |> Seq.filter (fun (_, firingTimeDiffs) -> not firingTimeDiffs.IsEmpty)
                |> Map.ofSeq

            frameIndex, firingTimeDiffsByConnection
        )
        // swap inner and outer keys and collect all the firing time diffs for each connection across frames
        |> Seq.fold (fun (state:Map<int, int list>) (frameIndex, firingTimeDiffsByConnection) ->
            firingTimeDiffsByConnection
            |> Map.fold (fun state connectionIndex firingTimeDiffs ->
                let totalFiringTimeDiffsByConnection =
                    if state.ContainsKey connectionIndex then
                        state.[connectionIndex]
                    else
                        []
                Map.add connectionIndex (List.append firingTimeDiffs totalFiringTimeDiffsByConnection) state
            ) state
        ) Map.empty
        // select the mode firing time diff
        |> Map.map (fun connectionIndex firingTimeDiffs ->
            let diffCountMap =
                firingTimeDiffs
                |> Seq.fold (fun (state:Map<int, int>) diff ->
                    let diffCount =
                        if state.ContainsKey diff then
                            state.[diff]
                        else
                            0
                    Map.add diff (diffCount + 1) state
                ) Map.empty
            let selectedDiff, _ =
                diffCountMap
                |> Map.toSeq
                |> Seq.maxBy (fun (diff, diffCount) -> diffCount)
            selectedDiff
        )

    /// For each neuron, get a list of synaptic weights for each post-synaptic connection
    /// with the weight list ordered by the connection delay band
    /// Return the data as a map indexed by neuron index
    member this.GetWeightsOrderedByDelayBand() =
        let GetWeightsByDelayForNeuron nindex (connections:List<Connection>) =
            seq {
                for delay in 1..20 do
                    yield this.GetConnectionsForNeuronAndDelay(nindex, delay)
            }
            |> Seq.concat
            |> Seq.map (fun connectionIndex -> connections.[connectionIndex-1].Weight)
            |> Seq.toList

        let connections = this.Connections   // copied with connection index remapped from 1-based to 0-based
        seq {
            for nindex in 0..this.TotalNeurons-1 do
                yield nindex, GetWeightsByDelayForNeuron nindex connections
        }
        |> Map.ofSeq

    /// For each neuron, get a list of synaptic weights for each post-synaptic connection
    /// with the weight list ordered by the connection delay band
    /// Save the data to the specified file
    member this.SaveWeightsOrderedByDelayBand(filePath) =
        let Stringify (data:Map<int, float list>) =
            data
            |> Map.toSeq
            |> Seq.map (fun (nindex, weights) -> sprintf "%d %s" nindex (String.Join(" ", weights)))

        let data = this.GetWeightsOrderedByDelayBand()
        File.WriteAllLines(filePath, Stringify data)

//---------------------------------------------------------------------------------------------------------
// Connection-related Methods

    /// Get the post-synaptic neurons from the specified connections
    static member GetPostSynapticNeurons(connections:seq<Connection>) =
        GetPostNeuronsFromConnections connections

    /// Get the connections between the specified group of neurons
    // MG generate a trigger pattern
    member this.GetTriggerGroupConnections(neurons) =
        let neuronSet = Set.ofSeq neurons
        this.Connections
        |> Seq.filter (fun connection -> (neuronSet.Contains connection.PreNeuron) && (neuronSet.Contains connection.PostNeuron))
        |> Seq.toList

    /// Show the connected post-synaptic neurons for the specified neurons
    member this.ShowPostSynapticNeurons(neurons) =
        let outgoingConnections = GetConnectionsByNeuron this (fun connection -> connection.PreNeuron)
        ShowConnectedPostNeurons neurons outgoingConnections

    /// Get the connected post-synaptic neurons for the specified neuron
    member this.GetPostSynapticNeurons(neuron) =
        let outgoingConnections = GetConnectionsByNeuron this (fun connection -> connection.PreNeuron)
        GetPostNeurons neuron outgoingConnections

    /// Get the connections that share a common post-synaptic neuron for the specified pre-synaptic neurons
    member this.GetConnectionsWithSharedPostNeurons(neurons) =
        let incomingConnections = GetConnectionsByNeuron this (fun connection -> connection.PostNeuron)
        let outgoingConnections = GetConnectionsByNeuron this (fun connection -> connection.PreNeuron)
        GetConnectionsWithSharedPostNeurons neurons incomingConnections outgoingConnections

    /// Get the incoming connections for the specified neuron
    member this.GetIncomingConnectionsForNeuron(neuron) =
        GetConnectionsByNeuron this (fun connection -> connection.PostNeuron)
        |> Map.toSeq
        |> Seq.filter (fun (postNeuron, connection) -> postNeuron = neuron)
        |> Seq.map (fun (postNeuron, connection) -> connection)
        |> Seq.concat
        |> Seq.toList

    /// Get the outgoing connections for the specified neuron
    member this.GetOutgoingConnectionsForNeuron(neuron) =
        GetConnectionsByNeuron this (fun connection -> connection.PreNeuron)
        |> Map.toSeq
        |> Seq.filter (fun (preNeuron, connection) -> preNeuron = neuron)
        |> Seq.map (fun (preNeuron, connection) -> connection)
        |> Seq.concat
        |> Seq.toList

    /// Show the incoming connections for the specified neuron
    member this.ShowIncomingConnections(neuron) =
        let incomingConnections = GetConnectionsByNeuron this (fun connection -> connection.PostNeuron)
        ShowConnections [ neuron ] incomingConnections

    /// Show the outgoing connections for the specified neuron
    member this.ShowOutgoingConnections(neuron) =
        let outgoingConnections = GetConnectionsByNeuron this (fun connection -> connection.PreNeuron)
        ShowConnections [ neuron ] outgoingConnections

    /// Select neurons that are post-synaptic to the specified neuron and with strong connections
    member this.SelectOutputNeuronsWithStrongConnections(neuron, minimumSynapticWeight, numberOfOutputs) =
        let outgoingConnections = this.GetOutgoingConnectionsForNeuron(neuron)
        outgoingConnections
        |> Seq.filter (fun connection -> connection.Weight >= minimumSynapticWeight)
        |> Seq.take numberOfOutputs
        |> CrossbarNetwork.GetPostSynapticNeurons

    /// Sample connections using the specified set of sample indices
    /// Use CrossbarNetwork.GenerateSampleIndices() to generate samples
    /// e.g. for excitatory connections only
    member this.SampleConnections(sampleSet:Set<int>) =

        this.Connections
        |> Seq.mapi (fun index connection -> index, connection)
        |> Seq.filter (fun (index, _) -> sampleSet.Contains index)
        |> Seq.map (fun (index, connection) -> connection)
        |> Seq.toArray

    /// Sample connections (both excitatory and inhibitory)
    member this.SampleConnections(numSamples) =
        
        let sampleSet:Set<int> = CrossbarNetwork.GenerateSampleIndices(numSamples, 0, this.TotalConnections)
        this.Connections
        |> Seq.mapi (fun index connection -> index, connection)
        |> Seq.filter (fun (index, _) -> sampleSet.Contains index)
        |> Seq.map (fun (index, connection) -> connection)
        |> Seq.toArray

//---------------------------------------------------------------------------------------------------------
// Static members

    static member internal AddActionHandlers(network:CrossbarNetwork, actionParameters:NetworkStateActionParameters) =

        let bindDefaultAction, performAction, actionInterval =
            actionParameters.BindDefaultAction, actionParameters.PerformAction, actionParameters.ActionInterval

        network.add_RunWarning(new RunWarningEventHandler(fun msg -> printfn "%s" msg))

        // default handler
        if bindDefaultAction then
            let oneSecTickHandler =
                fun sec frate maximal ifrate ->
                    if sec % 60 = 0 then        // report every one minute
                        printfn "%d %f %f %f" sec frate maximal ifrate
            network.add_OneSecondTickBeforeCleanup(new OneSecondTickBeforeCleanupEventHandler(oneSecTickHandler))

        if actionParameters.RepeatedActionBeforeCleanup.IsSome && performAction then
            // add another handler
            let handler sec frate maximal ifrate = actionParameters.RepeatedActionBeforeCleanup.Value sec frate maximal ifrate network
            let oneSecTickBeforeCleanupHandler =
                fun sec frate maximal ifrate ->
                    if sec % actionInterval = 0 then    // perform action every 'actionInterval' seconds
                        handler sec frate maximal ifrate
            network.add_OneSecondTickBeforeCleanup(new OneSecondTickBeforeCleanupEventHandler(oneSecTickBeforeCleanupHandler))

        if actionParameters.RepeatedActionAfterCleanup.IsSome && performAction then
            let handler sec = actionParameters.RepeatedActionAfterCleanup.Value sec network
            let oneSecTickAfterCleanupHandler =
                fun sec ->
                    if sec % actionInterval = 0 then    // perform action every 'actionInterval' seconds
                        handler sec
            network.add_OneSecondTickAfterCleanup(new OneSecondTickAfterCleanupEventHandler(oneSecTickAfterCleanupHandler))

        ()

    /// Create a grid network with optional high resolution data collector
    static member CreateGridNetwork(networkSpecifier:CrossbarNetworkSpecifier, hiResDataCollector:OneMillisecTickDataCollector option, verbose) =

        new CrossbarNetwork(networkSpecifier, hiResDataCollector, true, verbose)

    /// Create an ad hoc network with optional high resolution data collector
    static member CreateAdHocNetwork(networkSpecifier:CrossbarNetworkSpecifier, hiResDataCollector:OneMillisecTickDataCollector option, verbose) =

        let spn = new CrossbarNetwork(networkSpecifier, hiResDataCollector, false, verbose)
        spn.AddConnections(networkSpecifier.Connections.Value)
        spn

    /// Create a new grid network using the specified network state file and with optional high resolution data collector
    static member CreateGridNetwork(networkSpecifier:CrossbarNetworkSpecifier, pathToNetworkStateFile:string, hiResDataCollector:OneMillisecTickDataCollector option, plasticityDisabled, verbose) =
        let network = CrossbarNetwork.CreateGridNetwork(networkSpecifier, hiResDataCollector, verbose)
        network.LoadNetworkState(pathToNetworkStateFile)
        network.PlasticityDisabled <- plasticityDisabled
        network

    /// Read the state data for an N1000 CrossbarNetwork from the specified stream
    // assumes N1000 network
    static member CreateFromStream(streamReader:StreamReader) =
        let network = new CrossbarNetwork(CrossbarNetworkSpecifier.N1000Network, None, false, false)
        network.LoadNetworkState(streamReader)

        network

    /// Read the state data for a CrossbarNetwork of the specified size from the specified stream
    // assumes N1000 network
    static member CreateFromStream(networkSpecifier:CrossbarNetworkSpecifier, streamReader:StreamReader) =
        let network = new CrossbarNetwork(networkSpecifier, None, false, false)
        network.LoadNetworkState(streamReader)

        network

    /// Read the state data for an N1000 CrossbarNetwork from the specified state file
    // assumes N1000 network
    static member CreateFromFile(stateFilePath:string) =
        let network = new CrossbarNetwork(CrossbarNetworkSpecifier.N1000Network, None, false, false)
        network.LoadNetworkState(stateFilePath)

        network

    /// Read the state data for a CrossbarNetwork of the specified size from the specified state file
    static member CreateFromFile(networkSpecifier:CrossbarNetworkSpecifier, stateFilePath:string) =
        let network = new CrossbarNetwork(networkSpecifier, None, false, false)
        network.LoadNetworkState(stateFilePath)

        network

    /// Read the state data for a CrossbarNetwork of the specified size from the specified state file
    static member CreateFromCompressedFile(networkSpecifier:CrossbarNetworkSpecifier, stateFilePath:string) =
        let network = new CrossbarNetwork(networkSpecifier, None, false, false)
        network.LoadCompressedNetworkState(stateFilePath)

        network

    /// Create a CrossbarNetwork, mature it in the presence of random background firing and save the final network state
    static member CreateMatureNetwork(runSeconds, backgroundFiringRate, networkSpecifier:CrossbarNetworkSpecifier, outputFolder, networkName) =
        // create a final save action
        let networkAction sec network = CrossbarNetwork.SaveState(sec, network, outputFolder, networkName)
        let saveParameters = new NetworkStateActionParameters(false, 0, None, None, Some(networkAction))

        CrossbarNetwork.CreateMatureNetwork(runSeconds, backgroundFiringRate, networkSpecifier, saveParameters)

    /// Create a CrossbarNetwork, mature it in the presence of random background firing and save the network state at specified intervals
    // Create and save a mature network by running a new network for some hours with just random background stimulation
    // The normal value for runSeconds was previously five hours (3600 * 5 seconds); now two hours
    static member CreateMatureNetwork(runSeconds, backgroundFiringRate, networkSpecifier:CrossbarNetworkSpecifier, saveParameters:NetworkStateActionParameters) =

        let patternGenerator =
            let totalNeurons = networkSpecifier.NumExcitatoryNeurons + networkSpecifier.NumInhibitoryNeurons
            Some(new PoissonProcessPatternGenerator(totalNeurons, backgroundFiringRate))

        let network = new CrossbarNetwork(networkSpecifier)
        CrossbarNetwork.AddActionHandlers(network, saveParameters)      // for repeated actions (if any)

        CrossbarNetwork.RunNetworkWithFinalAction(network, runSeconds, patternGenerator, None, saveParameters)

    /// Save the network state of the specified network
    // Useful for composing a save action:
    //  let networkAction sec network = CrossbarNetwork.SaveState(sec, network, folder, baseName)
    //  let actionParameters = new NetworkStateActionParameters(performAction, actionInterval, networkAction, networkAction)
    static member SaveState(sec, network:CrossbarNetwork, outputFolder, baseName) =
        // by default, no state is saved at t = 0 secs (i.e. the untrained network state)
        // by default, the save filename uses seconds (formerly used minutes)
        if (sec > 0) then
            let filename = sprintf "%s%d.txt" baseName sec
            printf "Saving state as %s..."  filename
            // Note that network state must be saved AFTER cleanup
            network.SaveNetworkState(Path.Combine(outputFolder, filename))
            printfn "done"

    /// Run the network and perform a final action (any event-bound repeated action will also be executed)
    static member internal RunNetworkWithFinalAction(network:CrossbarNetwork, runSeconds, patternGenerator, stimulus, actionParameters:NetworkStateActionParameters) =

        let success =
            try
                RunNetwork network runSeconds patternGenerator stimulus
                true
            with
                | ex ->
                    printfn "Caught an exception: %s" (ex.Message)
                    let outputPath = Path.Combine(Environment.CurrentDirectory, "incomplete.txt")
                    printfn "Saving to %s" outputPath
                    network.SaveNetworkState(outputPath)
                    printfn "Network state saved"
                    false

        if success then
            if actionParameters.FinalAction.IsSome then
                let finalActionBeforeCleanup sec = actionParameters.FinalAction.Value sec network  // capture the network instance
                finalActionBeforeCleanup runSeconds

    /// Generate samples from the range inclusiveMinimum to exclusiveMaximum
    /// For use with sampling methods such as 
    static member GenerateSampleIndices(numSamples, inclusiveMinimum, exclusiveMaximum) =
        // sample without replacement
        let rnd = new Random(int(DateTime.Now.ToFileTimeUtc()))
        Seq.unfold (fun (state:Set<int>) ->
            let index = rnd.Next(inclusiveMinimum, exclusiveMaximum)
            let newState = if state.Contains index then state else state.Add index
            if newState.Count < numSamples then
                Some(index, newState)
            else
                None
        ) Set.empty
        |> Set.ofSeq
